bokusui について

ソフトウェアハウスでのPG・SEから始まり、10年近く勤めた金融系企業の社内SEを数年前にやめ、フリーランス時代を経たのち法人成りしました。システム開発の全工程をこじんまりとやり続けています。

特別徴収税額通知書がきた

 先日、市役所の方から「特別徴収税額通知書」なる書類が送られて来てました。
 
特別って、何??と思い中を開けると、どうやら源泉徴収の住民税版のようなものらしいのですが、いろいろ調べてみると、従業員が自分で住んでいる自治体に支払う「普通徴収」と、会社(事業所)の方が、従業員が住んでいる自治体に支払う「特別徴収」の2通りがあるようで、知らないうちに特別徴収義務者になっていたらしい。。

 封筒の中には、「特別徴収税額の決定通知書」と、6月分から来年5月分までの納付書が毎月分入っており、6月分は7月10日期限となっています。源泉徴収のように半年に一回支払いすれば済むものでは無いようなのですが、納付書があるのでまとめて先払いしちゃえばそれほど手間がかかるものでは無さそうです。とは言え、給料を払う前に税を払うという事は、経理上、源泉徴収とは逆の対処が必要そうなので、どうやるのかは別途調べないといけませんね。。

curlでパラメータを指定してWebページ上のファイルを取得してみる

 とあるWebサイトからファイルをダウンロードする作業を自動化したいという話があり、Linux上で動作するものという要件でしたので
とりあえずcurlでコマンドを作ってみました。対象サイトの認証はログイン画面からIDとパスワードを入力するという一般的な内容ですが、画面以外の対象サイトにおける認証の仕様については、何も情報が無くどうやらWebアプリ独自仕様のようです。
 ログイン画面でどんなやり取りがされているかと、FireFoxの開発ツールで確認し、POSTするパラメータを指定するコマンドを作成していきます。クッキーも指定してやってみますが、まだダメ。。

 今度はhttpヘッダの情報も開発ツールで確認しながら、必要そうなヘッダもcrulコマンドに指定すると、うまく動作しました。下記のような感じです。

ログイン画面

curl -X POST -L -b my.cookie -c my.cookie --header "Content-Type: application/x-www-form-urlencoded" -d "log=****&pwd=****" "https://targethost/login"

ダウンロードするファイルのGET

curl -L -b my.cookie -c my.cookie -o getout --header "Referer:https://targethost/aaaaa" https://targethost/files/targetfile.zip

 出来る出来ないはWebサイト側の作り次第かもしれませんが、とりあえずcurlでアクセスする事が出来そうです。
 とは言え、対象サイトの認証パスワードは定期更新が必要だし、他にもいろいろ対処が必要なので別の手段も考えた方がいいかもしれません。。。

backlog4jで課題を登録してみる

今度は、backlogへの課題登録を自動化したいという話があり、前回のredmineと似通った要件なので再利用箇所も多く、redmine java api部分をヌーラボ公認のbacklog4jに置き換えて実装してみました。
redmineとの違いはそれなりにありますが、backlog4jの方はKeyとIDを混同しやすい感じがします。例えば、課題のKeyはURLを見ればすぐ解りますが、IDの方は内部的なユニークな数値です。これに注意しながらbacklog用に前回のラッパークラスを置き換えてみました。

import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import com.nulabinc.backlog4j.BacklogClient;
import com.nulabinc.backlog4j.BacklogClientFactory;
import com.nulabinc.backlog4j.Category;
import com.nulabinc.backlog4j.Issue;
import com.nulabinc.backlog4j.IssueComment;
import com.nulabinc.backlog4j.IssueType;
import com.nulabinc.backlog4j.Project;
import com.nulabinc.backlog4j.Status;
import com.nulabinc.backlog4j.api.option.AddIssueCommentParams;
import com.nulabinc.backlog4j.api.option.CreateIssueParams;
import com.nulabinc.backlog4j.api.option.GetIssuesParams;
import com.nulabinc.backlog4j.api.option.UpdateIssueParams;
import com.nulabinc.backlog4j.conf.BacklogConfigure;
import com.nulabinc.backlog4j.conf.BacklogJpConfigure;
import com.nulabinc.backlog4j.conf.BacklogPackageConfigure;

public class BacklogRegist {
	
	private BacklogClient backlog;
		
	public BacklogRegist(String url, String apikey) throws MalformedURLException  {
		
		BacklogConfigure conf = null;
		//オンプレ環境
		if(url.startsWith("http")){
			conf = new BacklogPackageConfigure(url);
		}
		//クラウド
		else{
			conf = new BacklogJpConfigure(url);
		}
		conf.apiKey(apikey);
 		backlog = new BacklogClientFactory(conf).newClient();
				
		
	}
	/**
	 * 課題を取得する
	 * @param issueKey
	 * */
	public Issue getIssue(String issueKey){
		
		if(issueKey == null){
			return null;
		}
		return getIssue = backlog.getIssue(issueKey);
	}
	/**
	 * 課題を作成する
	 * @param projectKey
	 * @param  issueTypeName 種別名称
	 * @param categoryName カテゴリ名称
	 * @param summary 件名
	 * @param description 詳細
	 * @return ticketID 0は登録失敗
	 * */
	public long regist(String projectKey, String issueTypeName, String categoryName, 
						String summary, String description){
		
		CreateIssueParams param = new CreateIssueParams(
				getProjectID(projectKey), summary,
				getIssueTypeId(projectKey, issueTypeName), Issue.PriorityType.Normal);
		
		param.description(description);
		
		if(StringUtils.isNotEmpty(categoryName)){
			List<String> categoryIds = new ArrayList<String>();
			categoryIds.add(getCategoryId(projectKey, categoryName));		
			param.categoryIds(categoryIds);
		}
		
		Issue issue = backlog.createIssue(param);
		if(issue != null){
			return issue.getId();
		}
		return 0;
	}
	
	/**
	 * 課題を更新する(コメントの追加も可能)
	 * @param projectKey プロジェクトKey
	 * @param  issueIdorKey 課題IDまたはKey 
	 * @param status ステータス名称(更新しなければセットしない)
	 * @param addComment  更新なければセットしない
	 * @return commentID 0は登録失敗
	 * */
	public long update(String issueIdorKey, String status, String addComment){
		long ret = 0;
		Issue issue = null;
		
		if(StringUtils.isNotEmpty(status)){
			UpdateIssueParams param = new UpdateIssueParams(issueIdorKey);
			for( Status Entity: backlog.getStatuses()){
				if( Entity.getName().equals(status)){
					param.status(Entity.getStatusType());
				}
			}
			issue = backlog.updateIssue(param);
		}
		else{
			issue = backlog.getIssue(issueIdorKey);
		}
		if(StringUtils.isNotEmpty(addComment)){
			AddIssueCommentParams params = new AddIssueCommentParams(issue.getIssueKey(), addComment);

			IssueComment cmt = backlog.addIssueComment(params);
			ret = cmt.getId();
		}
		else{
			ret = issue.getId();
		}
		return ret;
	}

	/**
	 * 課題にコメントを追加する
	 * @param projectKey
	 * @param  issueKey
	 * @param addComment
	 * @return commentID 0は登録失敗
	 * */
	public long addComment(String issueKey, String addComment){
		Issue issue = backlog.getIssue(issueKey);
		
		if( issue != null){
			AddIssueCommentParams params = new AddIssueCommentParams(issue.getIssueKey(), addComment);

			IssueComment cmt = backlog.addIssueComment(params);
			return cmt.getId();
			
		}
		return 0;
	}

	/**
	 * 課題のタイプ(種別)を取得する
	 * @param prjKey プロジェクトキー
	 * @param name 種別名称
	 * */
	public String getIssueTypeId(String projectKey, String name){
		for( IssueType type: backlog.getIssueTypes(projectKey)){
			if( type.getName().equals(name)){
				return type.getIdAsString();
			}
		}
		return null;
	}
	
	/**
	 * カテゴリを取得する
	 * @param prjKey プロジェクトキー
	 * @param name 種別名称
	 * */
	public String getCategoryId(String projectKey, String name){
		for( Category type: backlog.getCategories(projectKey)){
			if( type.getName().equals(name)){
				return type.getIdAsString();
			}
		}
		return null;
	}
	/**
	 * プロジェクトIDを取得する
	 * @param prjKey プロジェクトキー
	 * @return Projectid
	 * */
	public String getProjectID(String projectKey){
		for( Project type: backlog.getProjects()){
			if( type.getProjectKey().equals(projectKey)){
				return type.getIdAsString();
			}
		}
		return null;
	}
}

上記を呼び出すのはこんな感じ

BacklogRegist backlog = new BacklogRegist("{URLまたはスペースID}","{apiKey}");

//チケット取得
Issue issue = backlog.getIssue("PROJECT-3");

//チケット登録
long issueId = backlog.regist("PROJECT", "要望", "設定変更",  
	"ユーザーの追加について", "○○さんを追加してください");

後日談
GetIssieで3ヶ月経って問題発生・・・たまに課題の取得に失敗するようになりました・・・
下記のようにしましたが、最初からそうしろ!って事ですね。でも、3ヶ月は何も問題なかったのですが・・
public Issue getIssue(String issueKey)

Issue getIssue = backlog.getIssue(issueKey);

redmine java api でチケットを取得してみる

 前回の続きで、今度はチケットを取得してみます。単純にチケットIDを指定して一つのチケットを取得するのでは無く、条件を指定し一括してチケットを取得して何かをするような要件への対応です。

 下記サンプルでは、クエリーを使用せずに、
ステータスID=2 かつ トラッカーID=10または11 かつ 題名に「テスト」を含む
という条件に一致するチケットを全て取得する内容です。
100件ずつチケットを取得して、100件以上のチケットがあればページ番号を変えて全チケットを取得します。

String apikey = "APIキー値";
String uri = "redmineサイトURI";

String TICKET_LIMIT = "100";

Map<String,String> params = new HashMap<String,String>();

// リミット
params.put("limit",TICKET_LIMIT);

params.put("status_id","2");

params.put("tracker_id","10|11");
//題名に「テスト」を含む
params.put("subject", "~テスト");

RedmineManager mgr = RedmineManagerFactory.createWithApiKey(uri, apikey);

boolean iscontinue = true;
int page = 1;
while(iscontinue){
	
	List<Issue> issues = mgr.getIssueManager().getIssues(params);
	
	for(Issue issue : issues){
		System.out.println(issue.getSubject());
	}
	if(issues.size() < Integer.valueOf(TICKET_LIMIT)){
		iscontinue = false;
	}
	else{
		page++;
		params.put("page",String.valueOf(page));
	}
}

redmine java api でチケットを登録してみる

 redmineへのチケット登録を自動化したいという話があり、内容的には常時トリガーを拾って登録という感じだったので、javaの常駐プロセスでトリガーを拾う事を前提として、こちらを使用して実装してみました。
https://github.com/taskadapter/redmine-java-api
なお、登録対象のredmineは2.5系でしたが、それほどバージョンを意識しなくても大丈夫そうです。

事前準備として、対象のredmineへapi登録用のユーザーを用意し、登録対象のプロジェクトへの権限を付与、個人設定画面からapiキーを取得します。

その他、プロジェクトidやトラッカーid等のredmine内部で持っているidの値はブラウザからapiで下記のようにidの値を確認してセットとなります。ただ、redmine java apiには、様々なマネージャーがあるので、id指定でなくても都度問い合わせしてidを取得できそうです。

ソース的には下記のようなラッパークラスを作って、登録や更新を呼び出し側で簡略化出来るようにしてます。

package jp.eosoro.redmine;

import java.util.ArrayList;
import java.util.List;

import com.taskadapter.redmineapi.Include;
import com.taskadapter.redmineapi.RedmineException;
import com.taskadapter.redmineapi.RedmineManager;
import com.taskadapter.redmineapi.RedmineManagerFactory;
import com.taskadapter.redmineapi.bean.CustomField;
import com.taskadapter.redmineapi.bean.Issue;
import com.taskadapter.redmineapi.bean.ProjectFactory;
import com.taskadapter.redmineapi.bean.TrackerFactory;
import com.taskadapter.redmineapi.bean.User;
import com.taskadapter.redmineapi.bean.UserFactory;

/**
 * Redmine登録クラス
 * */
public class RedmineRegist {

	RedmineManager mgr;
	
	private Issue issue;
	private List<CustomField> customeField;
	
	/**
	 * コンストラクタ
	 * @param String RedmineサイトURI
	 * @param String APIキー
	 */
	public RedmineRegist(String uri, String apiAccessKey) {
		mgr = RedmineManagerFactory.createWithApiKey(uri, apiAccessKey);

		issue = new Issue();
		customeField = new ArrayList<CustomField>();		
	}
	/**
	 * チケット登録
	 * @param subject タイトル
	 * @param description 詳細
	 * @param assigned_toId 担当者を指定する時はユーザーid 指定しない場合は0
	 * @param assigned_toGroupId グループを指定するときはグループid 指定しない場合は0
	 * @param parentid 親チケットを指定する時はチケットid 指定しない場合は0
	 * @param projectId
	 * @param trackerId 
	 * @param priorityId
	 * @param statusId
	 * @return int 登録チケットID 登録失敗時はゼロ
	 * */ 
	public int regist(String subject, String description, int assigned_toId, int assigned_toGroupId, int parentid, int projectId, int trackerId, int priorityId, int statusId){
		
		getIssue().setSubject(subject);
		
		getIssue().setDescription(description);

		//プロジェクト指定
		getIssue().setProject(ProjectFactory.create(projectId));

		//トラッカー指定
		getIssue().setTracker(TrackerFactory.create(trackerId));
		
		//ステータス/優先度指定
		getIssue().setPriorityId(priorityId);
		getIssue().setStatusId(statusId);
		
		// カスタムフィールド設定
		getIssue().addCustomFields(getCustomeField());
		
		try {
			//担当者指定
			if(assigned_toId > 0){
				getIssue().setAssignee(getMgr().getUserManager().getUserById(assigned_toId));
				
			}
			//グループ
			else if(assigned_toGroupId > 0){
				User u = UserFactory.create(assigned_toGroupId);
				getIssue().setAssignee(u);
			}
			//親チケット
			if(parentid > 0){
				getIssue().setParentId(parentid);
			}
			return getMgr().getIssueManager().createIssue(getIssue()).getId();

		} catch (RedmineException e) {
			return 0;
		}
	}
	/**
	 * 関連するチケットをセットする
	 * @param int チケットID
	 * @throws RedmineException 
	 * */
	public void setRelation(int id, int relateid) throws RedmineException{
		getMgr().getIssueManager().createRelation(id, relateid, "relates");
		
	}
	/**
	 * ジャーナル追加
	 * @param id チケットID
	 * @param Notes コメント
	 * @throws RedmineException 
	 * */
	public int addJournal(int id, String Notes) throws RedmineException{
		Issue issue =  getMgr().getIssueManager().getIssueById(id, Include.journals);
		
		issue.setNotes(Notes);

		getMgr().getIssueManager().update(issue);

		return issue.getId();
	}

	public RedmineManager getMgr() {
		return mgr;
	}
	private Issue getIssue() {
		return issue;
	}
	public List<CustomField> getCustomeField() {
		return customeField;
	}
	public void setCustomeField(List<CustomField> customeField) {
		this.customeField = customeField;
	}
}

下記の呼び出し側では、親チケットを作成してジャーナルを追加、子チケットを2つ作成し関連付けてます。

       RedmineRegist  mine = new RedmineRegist (url, apikey);
       
       int parentId = mine.regist("親チケット", "親チケット本文", 0, 0, 0, 4, 4, 2, 1);
       
       mine.addJournal(parentId, "ジャーナル");

       int childId = mine.regist("子チケット", "子チケット本文",  0, 0, parentId, 4, 4, 2, 1);
       
       int relationId = mine.regist("関連チケット", "関連チケット本文",  0, 0, parentId, 4, 4, 2, 1);
       
       
       mine.setRelation(childId, relationId);

いろいろやってみましたが、redmineに対して殆どの事は出来そうです。

e-Taxで法人税の中間申告&納税してみた。

中間申告の通知が来ていたので、e-Taxで納税してみました。

e-Taxは何回も使ってますが、中間申告は今回初めて。
確定申告の期間なので、e-Taxが土日もやっており助かります。

e-Taxにログインし、申告・申請・納税を押して、新規作成
で次はどこ押すの?と立ち止まる。

正解は、「納税情報を登録する」なのだが、
中間申告の通知を送ってきてるんだから、既に税務署にデータは登録されているんじゃないの?
と疑問を感じつつ先へ進む。税務署は初期表示されているので、次へ
作成方法の選択と来て、「新規作成」を選択し、次へ
税目を「法人税」と選択し、次へ
課税期間を送られてきた通知の通り入力し、「中間申告」を選択し、次へ
通知に書かれていた金額を「本税」に入力し、次へ
ここで入力内容の確認画面となり、確認後、次へ
受付システムへの送信となり、送信
「送信が完了しました」と出て、受信通知の確認を押す。
送信されたデータを受け付けたと表示され、画面下の方に、電子納税とあるので、今すぐ納付でやると今日は土曜でダメ、納付日を指定される方というボタンを押すと、登録済の口座情報が表示され、納付ボタンを押して終了です。

これって、画面4つ程度で事足りるし、その方が解りやすい気がしますが、どうなんでしょう?
昔、画面1ついくらみたいな開発費用の見積もりをした事もありましたが、今もそうなのかなあ?

なお、このオペレーションを法人税と地方法人税で2回やって、中間申告&納税手続きは終了です。

JAX-RSでWeb画面にドラッグアンドドロップされたファイルを読み込む

テキストファイルをサーバへアップロードしたいという要件があり、Web画面からファイルをドラッグアンドドロップできるようにして、それをJAX-RSで処理する事にしてみました。

下記の参考にさせて頂き、それらを組合わせただけと言えばだけですが。。

Jersey(JAX-RS)でファイルアップロード
HTML5 の File API でドラッグ&ドロップする

まず、JAX-RSのルートパスを指定します

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class RestApplication extends Application {
//何も書く事はありません・・・
}

次にファイルを処理するJAX-RS部分を作ります

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Stream;

import javax.enterprise.context.RequestScoped;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataParam;

@RequestScoped
@Path("/upload")
public class FileUploader {
	
	@POST
	@Consumes(MediaType.MULTIPART_FORM_DATA)
	public Response post(@FormDataParam("file") InputStream fileStream,
					@FormDataParam("file") FormDataContentDisposition fileDisposition) {

		int statusCode = 200;
		String out = "";

		//テキストファイルの読み込み
		try (BufferedReader br = new BufferedReader(new InputStreamReader(fileStream))) {
			try(Stream<String> lines = br.lines()){
				//1行毎の処理は省略
			}
        } catch (IOException e) {
			statusCode = 400;
			out = e.getMessage();
		}
		return Response.status(statusCode).type("text/html;charset=Shift-JIS").
			entity(out).
			build();
	}
}

これでファイルアップローダーのJAX-RSパスは、{コンテンツルート}/api/uploadになりました。

続いてJavaScriptの部分(殆ど上記参考から持ってきただけです。。)

$(function() {
   var droppable = $("#droppable");

    // イベントをキャンセルするハンドラです.
    var cancelEvent = function(event) {
        event.preventDefault();
        event.stopPropagation();
        return false;
    }

    // dragenter, dragover イベントのデフォルト処理をキャンセルします.
    droppable.bind("dragenter", cancelEvent);
    droppable.bind("dragover", cancelEvent);

    // ドロップ時のイベントハンドラを設定します.
    var handleDroppedFile = function(event) {

	    var dropfile = event.originalEvent.dataTransfer.files[0];
	    
	    var formData = new FormData();
	    formData.append( 'file', dropfile );
	      
	    var hostUrl= 'api/upload';
	    $.ajax({
	       url: hostUrl,
	       method: 'post',
	       dataType: 'json',
	       data: formData,
	       processData: false,
	       contentType: false,
	       timeout:100000
	    }).done(function(data) {
        	alert( '正常に終了しました');
	    }).fail(function( jqXHR, textStatus, errorThrown ) {
	         alert( 'エラーが発生しました\n'+ jqXHR.responseText);
	    });
	
	    // デフォルトの処理をキャンセルします.
	    cancelEvent(event);
	    return false;
    }
    // ドロップ時のイベントハンドラを設定します.
    droppable.bind("drop", handleDroppedFile);
});

最後にHTML部分のドラッグアンドドロップ部分です。
ここにドロップされたファイルがJAX-RSの処理箇所のInputStream に繋がります。

<div class="droppable" id="droppable" style="border:gray solid 1em; width:100px; padding:2em;">アップロードするファイルはココにドロップしてください。</div>      

これでとりあえずはJAX-RSでファイル処理が出来ることが確認出来ました。

最後にPOM。

<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>javax.ws.rs-api</artifactId>
    <version>2.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.jersey.contribs/jersey-multipart -->
<dependency>
    <groupId>com.sun.jersey.contribs</groupId>
    <artifactId>jersey-multipart</artifactId>
    <version>1.19.3</version>
</dependency>

bootstrap2 datetimepickerを1分単位にする

ちょっと前に作ったJavaEEアプリでbootstrap2のdatatimepickerを使ったのだが、
?p=698
5分刻みは使いにくいという話が出てきて、ググッてみたところ下記を発見。
http://www.malot.fr/bootstrap-datetimepicker/
オプションのminuteStepで値を変えられるみたいなのでやってみました。
1分刻みにするとこうなります。

xhtml側は下記minuteStepのオプションを追加するだけ

<h:outputScript name="bootstrap/js/bootstrap-datetimepicker.min.js" />
<h:outputScript name="bootstrap/js/bootstrap-datetimepicker.ja.js" />
<h:outputStylesheet name="bootstrap/css/bootstrap-datetimepicker.min.css" />

$(function() {
  $('.datetimepicker').datetimepicker({
    format: 'yyyy/mm/dd hh:ii:ss',
    autoclose: true,
    todayBtn: true,
    pickerPosition: "bottom-left",
    minuteStep: 1,  
    language: 'ja'
   });
});

デフォルトだと5分刻みなのでこんな感じ

ダイアログが長くなって画面ギリギリだけど、この方が便利ということでこちらでリリース
次作るものはbootstrap3にしないとなあ。。

法人宛に送られてきたもの

法人成りして暫くは近所の会計事務所や税務署の書類くらいしかポストに入っていませんでしたが、2年目に入りこんな書類が来ていたなあという整理です。

・企業情報調査票
 初年の決算終了後に東京商工リサーチから送られてきたもの。財務諸表とかかなり細かい情報を記入するようになっている。特に信用情報を確認するような相手との取引は無いので、未返信。

・厚生年金保険・健康保険の加入状況にかかる調査
 日本年金機構から来たアンケート。まだ国民健康保険のままですが、正直に書いて返信

・事業所・企業照会票
 総務省統計局から来た事業状況を確認する書類。こちらはWebから記入出来たのでそちらで対処。しかし、FireFoxだと政府のSSL証明書が安全でないとでるんですが何とかしないのでしょうか?

・消費税の転嫁拒否等に関する調査
 こちらは、中小企業庁から法人取引でちゃんと消費税を転嫁できているかの調査票。してくれない取引先も記入可だが、継続して取引しているところは書きにくいよね。筆者としてはまったく問題無いので返信してません。個人事業主のころから毎年来ている書類ですが、最初のうちは返信してました。

・償却資産申告書
 ちょっと前に市から来ていた償却資産所有の申告書。来年1月末までに届けるものらしいが、要領を見る限り、該当する資産が無ければ何もしなくて良さそう。

→後日談
償却資産申告書は何も無いから何も送らなかったのですが、3月になって、「ちゃんと提出してね」的な書面が届いてました。
無いものは無いので、電話で役所に連絡したところ、電話レベルで「無い」という事が確認できたのでOKです、と回答が得られました。

OracleDBで文字化け発生

今構築中のPHP+apache+OracleDB構成のWeb業務アプリですが、
テスト環境を作ってもらったので動かしてみたところ、全角文字が??に化けて困ったことに。。

アプリケーションの要件上、2つのOracleDB(どちらも12c)に同時接続する必要があり、
一方の文字コードが
NLS_LANG=Japanese_Japan.JA16SJISTILDE で、
もう一方が、
NLS_LANG=Japanese_Japan.AL32UTF8 となっています。

とは言え、DBクライアントとなるphp側としては、Japanese_Japan.AL32UTF8でいいはず。

apacheのhttpd.confを見ると、
setEnv NLS_LANG Japanese_Japan.AL32UTF8
としているのですが、どうも有効にならない模様。

結果、apacheの起動シェルに
export NLS_LANG=Japanese_Japan.AL32UTF8
を入れてもらって解消。
念の為、はしご高とかを画面から入れてみましたが問題無し。

昔(Oracle8とか)は第2水準以上の漢字はトラブルの元だったとの記憶がありますが、今は便利になりましたね。